package bancosys.tec.persist.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.security.auth.Subject;

import jmine.tec.persist.PersistMessages;
import jmine.tec.persist.transaction.CallableAdapter;
import jmine.tec.persist.transaction.TransactionalAction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.AuthorizationManager;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.spring.transaction.PartitionTransactionDefinition;

/**
 * Controller com funcionalidades para gerenciamento transacional.
 * 
 * @created 05/03/2008
 * @author Gustavo Almeida
 */
@SuppressWarnings("deprecation")
public class TransactionalController extends AbstractPersistenceEnabledController implements DisposableBean {
    private static final Log LOG = LogFactory.getLog(TransactionalController.class);

    private static final ThreadLocal<List<TransactionStatus>> THREAD_LOCAL_TRANSACTIONS = new ThreadLocal<List<TransactionStatus>>();

    private static final ThreadLocal<PartitionTransactionDefinition> LOCAL_TRANSACTION_DEFINITION =
            new ThreadLocal<PartitionTransactionDefinition>();

    protected PlatformTransactionManager transactionManager;

    private final ExecutorService executor = Executors.newCachedThreadPool();

    /**
     * Construtor.
     */
    public TransactionalController() {
        super();
    }

    /**
     * Construtor.
     * 
     * @param sf session factory
     * @param transactionManager TransacionManager
     */
    public TransactionalController(SessionFactory sf, PlatformTransactionManager transactionManager) {
        this.sessionFactory = sf;
        this.transactionManager = transactionManager;
    }

    /**
     * Starts a transaction and bounds to the current thread.
     */
    public void startTransaction() {
        PartitionTransactionDefinition txDefinition = getCurrentTransactionDefinition();
        List<TransactionStatus> transacoesLocais = this.safeGetTransacoesLocais();
        TransactionStatus tx = this.transactionManager.getTransaction(txDefinition);
        transacoesLocais.add(tx);
        LOG.debug("No. de transacoes abertas: " + transacoesLocais.size());
        THREAD_LOCAL_TRANSACTIONS.set(transacoesLocais);
    }

    /**
     * Cria uma nova {@link PartitionTransactionDefinition}
     * 
     * @return {@link PartitionTransactionDefinition}
     */
    private static PartitionTransactionDefinition newTransactionDefinition() {
        PartitionTransactionDefinition txDefinition = new PartitionTransactionDefinition();
        txDefinition.setPartition(AuthorizationContext.hot);
        txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        txDefinition.setPureSessionOnly(false);
        return txDefinition;
    }

    /**
     * Devolve a lista de transações locais.
     * 
     * @return lista de transações locais, lista vazia quando não houver nenhuma.
     */
    private List<TransactionStatus> safeGetTransacoesLocais() {
        List<TransactionStatus> transacoesLocais = THREAD_LOCAL_TRANSACTIONS.get();
        if (transacoesLocais == null) {
            transacoesLocais = new ArrayList<TransactionStatus>();
        }
        return transacoesLocais;
    }

    /**
     * Commits the transaction bounded to the current thread.
     */
    public void commit() {
        List<TransactionStatus> transacoesLocais = this.safeGetTransacoesLocais();
        if (transacoesLocais.size() == 0) {
            throw new PersistenceException(PersistMessages.NO_TRANSACTION_FOR_CONCURRENT_THREAD_UNABLE_TO_COMMIT.create());
        }
        TransactionStatus tx = transacoesLocais.get(transacoesLocais.size() - 1);
        if (tx == null) {
            throw new PersistenceException(PersistMessages.NO_TRANSACTION_FOR_CONCURRENT_THREAD_UNABLE_TO_COMMIT.create());
        }

        try {
            this.getSessionFactory().getCurrentSession().flush();
        } catch (InvalidStateException e) {
            this.transactionManager.rollback(tx);
            transacoesLocais.remove(transacoesLocais.size() - 1);
            THREAD_LOCAL_TRANSACTIONS.set(transacoesLocais);
            LOG.debug("No. de transacoes abertas (apos commit): " + transacoesLocais.size());

            InvalidValue firstIv = e.getInvalidValues()[0];
            Object bean = firstIv.getBean();
            String beanName = bean.getClass().getSimpleName();
            if (bean.getClass().isAnnotationPresent(DisplayName.class)) {
                beanName = bean.getClass().getAnnotation(DisplayName.class).value();
            }

            StringBuilder msg = new StringBuilder();
            msg.append(firstIv.getPropertyName()).append(": ").append(firstIv.getMessage());
            for (int i = 1; i < e.getInvalidValues().length; i++) {
                InvalidValue iv = e.getInvalidValues()[i];
                msg.append(", ").append(iv.getPropertyName()).append(": ").append(iv.getMessage());
            }

            throw new PersistenceException(PersistMessages.VALIDATION_ERROR_HIBERNATE_VALIDATION_2.create(beanName, msg), e);
        } catch (Throwable e) {
            this.transactionManager.rollback(tx);
            transacoesLocais.remove(transacoesLocais.size() - 1);
            THREAD_LOCAL_TRANSACTIONS.set(transacoesLocais);
            LOG.debug("No. de transacoes abertas (apos commit): " + transacoesLocais.size());
            throw new PersistenceException(PersistMessages.ERROR_DURING_FLUSH_UNABLE_TO_COMMIT.create(), e);
        }
        try {
            this.transactionManager.commit(tx);
        } finally {
            transacoesLocais.remove(transacoesLocais.size() - 1);
            LOG.debug("No. de transacoes abertas (apos commit): " + transacoesLocais.size());
            THREAD_LOCAL_TRANSACTIONS.set(transacoesLocais);
        }
    }

    /**
     * Rollbacks the transaction bounded to the current thread.
     */
    public void rollback() {
        List<TransactionStatus> transacoesLocais = this.safeGetTransacoesLocais();
        if (transacoesLocais.size() == 0) {
            throw new PersistenceException(PersistMessages.NO_TRANSACTION_FOR_CONCURRENT_THREAD_UNABLE_TO_COMMIT.create());
        }
        TransactionStatus tx = transacoesLocais.get(transacoesLocais.size() - 1);
        if (tx == null) {
            throw new PersistenceException(PersistMessages.NO_TRANSACTION_FOR_CONCURRENT_THREAD_UNABLE_TO_COMMIT.create());
        }
        try {
            this.transactionManager.rollback(tx);
        } finally {
            transacoesLocais.remove(transacoesLocais.size() - 1);
            LOG.debug("No. de transacoes abertas (apos rollback): " + transacoesLocais.size());
            THREAD_LOCAL_TRANSACTIONS.set(transacoesLocais);
        }
    }

    /**
     * Testa se existe transação inicializada.
     * 
     * @return <code>true</code> se existe transação inicializada, <code>false</code> caso contrario.
     */
    public boolean hasTransaction() {
        return this.safeGetTransacoesLocais().size() > 0;
    }

    /**
     * Define o contexto transacional: sessionfactory e transactionManager.
     * 
     * @param otherController outro TransactionalController
     */
    public void setContext(TransactionalController otherController) {
        this.sessionFactory = otherController.getSessionFactory();
        this.transactionManager = otherController.getTransactionManager();
    }

    /**
     * Devolve o Transaction Manager.
     * 
     * @return Transaction Manager.
     */
    public PlatformTransactionManager getTransactionManager() {
        return this.transactionManager;
    }

    /**
     * Define o transaction manager.
     * 
     * @param transactionManager transaction manager.
     */
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    /**
     * Executa uma ação dentro de uma transação.
     * 
     * @param action ação
     * @param <T> Tipo do dado retornado pela ação
     * @return o dado retornado pela ação
     * @throws Exception se ocorrer algum erro ao executar a ação
     * @deprecated usar {@link #executeTransacted(Callable)} no lugar
     */
    @Deprecated
    public <T> T executeTransacted(TransactionalAction<T> action) throws Exception {
        return this.executeTransacted(new CallableAdapter<T>(action));
    }

    /**
     * Executa uma ação dentro de uma transação em uma Thread separada (usando ou nao um thread pool)
     * 
     * @param action ação
     * @param <T> Tipo do dado retornado pela ação
     * @return o dado retornado pela ação
     * @throws Exception se ocorrer algum erro ao executar a ação
     */
    public <T> T executeTransacted(Callable<T> action) throws Exception {
        try {
            return this.submitTransacted(action).get();
        } catch (ExecutionException e) {
            if (e.getCause() instanceof Exception) {
                throw (Exception) e.getCause();
            } else {
                throw new IllegalStateException(e.getCause());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw e;
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * Submete a execução de uma ação dentro de uma transação em uma thread apartada. Esta execução herda da thread que faz a chamada o
     * Subject (security) status de autorização.
     * 
     * @param <T> tipo do dado retornado pela ação
     * @param action ação
     * @return future
     */
    public <T> Future<T> submitTransacted(Callable<T> action) {
        return this.executor.submit(new TransactedCallable<T>(action, this.securityService.getCurrentThreadSubject()));
    }

    /**
     * Devolve a {@link PartitionTransactionDefinition} associada a thread atual, criando uma se necessario
     * 
     * @return {@link PartitionTransactionDefinition}
     */
    public static PartitionTransactionDefinition getCurrentTransactionDefinition() {
        PartitionTransactionDefinition definition = LOCAL_TRANSACTION_DEFINITION.get();
        if (definition != null) {
            return definition;
        }
        return newTransactionDefinition();
    }

    /**
     * Seta a {@link PartitionTransactionDefinition} que deve ser usada para transacoes desta thread a partir de agora. A transacao deve ser
     * restaurada atraves do metodo {@link #restoreCurrentThreadTransactionDefinition(PartitionTransactionDefinition)}
     * 
     * @param txDef {@link PartitionTransactionDefinition}
     * @return a {@link PartitionTransactionDefinition} anterior.
     */
    public static PartitionTransactionDefinition setCurrentTransactionDefinition(PartitionTransactionDefinition txDef) {
        PartitionTransactionDefinition oldTx = getCurrentTransactionDefinition();
        LOCAL_TRANSACTION_DEFINITION.set(txDef);
        return oldTx;
    }

    /**
     * Restaura a {@link PartitionTransactionDefinition} anterior
     * 
     * @param previous {@link PartitionTransactionDefinition}
     */
    public static void restoreCurrentThreadTransactionDefinition(PartitionTransactionDefinition previous) {
        LOCAL_TRANSACTION_DEFINITION.set(previous);
    }

    /**
     * {@inheritDoc}
     */
    public void destroy() throws Exception {
        this.executor.shutdownNow();
    }

    /**
     * Callable que recebe um Callable delegate e executa com uma transacao ativa.
     * 
     * @author takeshi
     * @param <T> o tipo do callable
     */
    private class TransactedCallable<T> implements Callable<T> {

        private final Callable<T> delegate;

        private final Subject subject;

        private final AuthorizationStatus currentAuthStatus;

        /**
         * @param delegate {@link Callable}
         * @param currentSubject o usuario atual
         */
        public TransactedCallable(Callable<T> delegate, Subject currentSubject) {
            this.delegate = delegate;
            this.subject = currentSubject;
            this.currentAuthStatus = AuthorizationManager.getCurrentThreadAuthorizationStatus();
        }

        /**
         * {@inheritDoc}
         */
        public T call() throws Exception {
            boolean rollback = true;
            TransactionalController.this.startTransaction();
            try {
                TransactionalController.this.securityService.setCurrentThreadSubject(this.subject);
                AuthorizationManager.setCurrentThreadAuthorizationStatus(this.currentAuthStatus);
                T returnValue = this.delegate.call();
                rollback = false;
                return returnValue;
            } finally {
                TransactionalController.this.securityService.setCurrentThreadSubject(null);
                AuthorizationManager.setCurrentThreadAuthorizationStatus(null);
                if (rollback) {
                    TransactionalController.this.rollback();
                } else {
                    TransactionalController.this.commit();
                }
            }
        }

    }

}
